/**
 * @author alteredq / http://alteredqualia.com/
 */

THREE.PathControls = function (object, domElement) {

    this.object = object;
    this.domElement = (domElement !== undefined) ? domElement : document;

    this.id = "PathControls" + THREE.PathControlsIdCounter++;

    // API

    this.duration = 10 * 1000; // milliseconds
    this.waypoints = [];

    this.useConstantSpeed = true;
    this.resamplingCoef = 50;

    this.debugPath = new THREE.Object3D();
    this.debugDummy = new THREE.Object3D();

    this.animationParent = new THREE.Object3D();

    this.lookSpeed = 0.005;
    this.lookVertical = true;
    this.lookHorizontal = true;
    this.verticalAngleMap = {srcRange: [0, 2 * Math.PI], dstRange: [0, 2 * Math.PI]};
    this.horizontalAngleMap = {srcRange: [0, 2 * Math.PI], dstRange: [0, 2 * Math.PI]};

    // internals

    this.target = new THREE.Object3D();

    this.mouseX = 0;
    this.mouseY = 0;

    this.lat = 0;
    this.lon = 0;

    this.phi = 0;
    this.theta = 0;

    var PI2 = Math.PI * 2;

    this.viewHalfX = 0;
    this.viewHalfY = 0;

    if (this.domElement !== document) {

        this.domElement.setAttribute('tabindex', -1);

    }

    // methods

    this.handleResize = function () {

        if (this.domElement === document) {

            this.viewHalfX = window.innerWidth / 2;
            this.viewHalfY = window.innerHeight / 2;

        } else {

            this.viewHalfX = this.domElement.offsetWidth / 2;
            this.viewHalfY = this.domElement.offsetHeight / 2;

        }

    };

    this.update = function (delta) {

        var srcRange, dstRange;

        if (this.lookHorizontal) this.lon += this.mouseX * this.lookSpeed * delta;
        if (this.lookVertical) this.lat -= this.mouseY * this.lookSpeed * delta;

        this.lon = Math.max(0, Math.min(360, this.lon));
        this.lat = Math.max(-85, Math.min(85, this.lat));

        this.phi = THREE.Math.degToRad(90 - this.lat);
        this.theta = THREE.Math.degToRad(this.lon);

        this.phi = normalize_angle_rad(this.phi);

        // constrain vertical look angle

        srcRange = this.verticalAngleMap.srcRange;
        dstRange = this.verticalAngleMap.dstRange;

        var tmpPhi = THREE.Math.mapLinear(this.phi, srcRange[0], srcRange[1], dstRange[0], dstRange[1]);
        var tmpPhiFullRange = dstRange[1] - dstRange[0];
        var tmpPhiNormalized = (tmpPhi - dstRange[0]) / tmpPhiFullRange;

        this.phi = QuadraticEaseInOut(tmpPhiNormalized) * tmpPhiFullRange + dstRange[0];

        // constrain horizontal look angle

        srcRange = this.horizontalAngleMap.srcRange;
        dstRange = this.horizontalAngleMap.dstRange;

        var tmpTheta = THREE.Math.mapLinear(this.theta, srcRange[0], srcRange[1], dstRange[0], dstRange[1]);
        var tmpThetaFullRange = dstRange[1] - dstRange[0];
        var tmpThetaNormalized = (tmpTheta - dstRange[0]) / tmpThetaFullRange;

        this.theta = QuadraticEaseInOut(tmpThetaNormalized) * tmpThetaFullRange + dstRange[0];

        var targetPosition = this.target.position,
            position = this.object.position;

        targetPosition.x = 100 * Math.sin(this.phi) * Math.cos(this.theta);
        targetPosition.y = 100 * Math.cos(this.phi);
        targetPosition.z = 100 * Math.sin(this.phi) * Math.sin(this.theta);

        this.object.lookAt(this.target.position);

    };

    this.onMouseMove = function (event) {

        if (this.domElement === document) {

            this.mouseX = event.pageX - this.viewHalfX;
            this.mouseY = event.pageY - this.viewHalfY;

        } else {

            this.mouseX = event.pageX - this.domElement.offsetLeft - this.viewHalfX;
            this.mouseY = event.pageY - this.domElement.offsetTop - this.viewHalfY;

        }

    };

    // utils

    function normalize_angle_rad(a) {

        var b = a % PI2;
        return b >= 0 ? b : b + PI2;

    };

    function distance(a, b) {

        var dx = a[0] - b[0],
            dy = a[1] - b[1],
            dz = a[2] - b[2];

        return Math.sqrt(dx * dx + dy * dy + dz * dz);

    };

    function QuadraticEaseInOut(k) {

        if ((k *= 2) < 1) return 0.5 * k * k;
        return -0.5 * (--k * (k - 2) - 1);

    };

    function bind(scope, fn) {

        return function () {

            fn.apply(scope, arguments);

        };

    };

    function initAnimationPath(parent, spline, name, duration) {

        var animationData = {

            name: name,
            fps: 0.6,
            length: duration,

            hierarchy: []

        };

        var i,
            parentAnimation, childAnimation,
            path = spline.getControlPointsArray(),
            sl = spline.getLength(),
            pl = path.length,
            t = 0,
            first = 0,
            last = pl - 1;

        parentAnimation = {parent: -1, keys: []};
        parentAnimation.keys[first] = {time: 0, pos: path[first], rot: [0, 0, 0, 1], scl: [1, 1, 1]};
        parentAnimation.keys[last] = {time: duration, pos: path[last], rot: [0, 0, 0, 1], scl: [1, 1, 1]};

        for (i = 1; i < pl - 1; i++) {

            // real distance (approximation via linear segments)

            t = duration * sl.chunks[i] / sl.total;

            // equal distance

            //t = duration * ( i / pl );

            // linear distance

            //t += duration * distance( path[ i ], path[ i - 1 ] ) / sl.total;

            parentAnimation.keys[i] = {time: t, pos: path[i]};

        }

        animationData.hierarchy[0] = parentAnimation;

        THREE.AnimationHandler.add(animationData);

        return new THREE.Animation(parent, name, THREE.AnimationHandler.CATMULLROM_FORWARD, false);

    };


    function createSplineGeometry(spline, n_sub) {

        var i, index, position,
            geometry = new THREE.Geometry();

        for (i = 0; i < spline.points.length * n_sub; i++) {

            index = i / (spline.points.length * n_sub);
            position = spline.getPoint(index);

            geometry.vertices[i] = new THREE.Vector3(position.x, position.y, position.z);

        }

        return geometry;

    };

    function createPath(parent, spline) {

        var lineGeo = createSplineGeometry(spline, 10),
            particleGeo = createSplineGeometry(spline, 10),
            lineMat = new THREE.LineBasicMaterial({color: 0xff0000, linewidth: 3}),
            lineObj = new THREE.Line(lineGeo, lineMat),
            particleObj = new THREE.ParticleSystem(particleGeo, new THREE.ParticleBasicMaterial({
                color: 0xffaa00,
                size: 3
            }));

        lineObj.scale.set(1, 1, 1);
        parent.add(lineObj);

        particleObj.scale.set(1, 1, 1);
        parent.add(particleObj);

        var waypoint,
            geo = new THREE.SphereGeometry(1, 16, 8),
            mat = new THREE.MeshBasicMaterial({color: 0x00ff00});

        for (var i = 0; i < spline.points.length; i++) {

            waypoint = new THREE.Mesh(geo, mat);
            waypoint.position.copy(spline.points[i]);
            parent.add(waypoint);

        }

    };

    this.init = function () {

        // constructor

        this.spline = new THREE.Spline();
        this.spline.initFromArray(this.waypoints);

        if (this.useConstantSpeed) {

            this.spline.reparametrizeByArcLength(this.resamplingCoef);

        }

        if (this.createDebugDummy) {

            var dummyParentMaterial = new THREE.MeshLambertMaterial({color: 0x0077ff}),
                dummyChildMaterial = new THREE.MeshLambertMaterial({color: 0x00ff00}),
                dummyParentGeo = new THREE.CubeGeometry(10, 10, 20),
                dummyChildGeo = new THREE.CubeGeometry(2, 2, 10);

            this.animationParent = new THREE.Mesh(dummyParentGeo, dummyParentMaterial);

            var dummyChild = new THREE.Mesh(dummyChildGeo, dummyChildMaterial);
            dummyChild.position.set(0, 10, 0);

            this.animation = initAnimationPath(this.animationParent, this.spline, this.id, this.duration);

            this.animationParent.add(this.object);
            this.animationParent.add(this.target);
            this.animationParent.add(dummyChild);

        } else {

            this.animation = initAnimationPath(this.animationParent, this.spline, this.id, this.duration);
            this.animationParent.add(this.target);
            this.animationParent.add(this.object);

        }

        if (this.createDebugPath) {

            createPath(this.debugPath, this.spline);

        }

        this.domElement.addEventListener('mousemove', bind(this, this.onMouseMove), false);

    };

    this.handleResize();

};

THREE.PathControlsIdCounter = 0;
